package com.tbit;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;


/**
 * 版本上报
 *
 * @author <a href="wei.xing@tbit.com.cn">wei.xing</a>
 * @version 1.5
 * @see <a href="http://adsl1.tbit.com.cn:21490/pages/viewpage.action?pageId=112362187">Version 要求与说明</a>
 * @since 2024/1/29
 */
@Slf4j
@Component
@EnableScheduling
public class Version {
    @Autowired(required = false)
    private ServletWebServerApplicationContext applicationContext;

    /**
     * version组件版本号
     */
    public static final String VER_COMP_VERSION = "1.5";

    /**
     * 版本号
     */
    public static final String VERSION = "5.1.0";
    /**
     * 服务名称
     */
    private static final String NAME = "main";

    /**
     * token
     */
    private static final String TOKEN = "b00806a8b4d34d87a5ddd7c656686ab7";

    private static final String SIGN_KEY = "sign";

    private static final String TIMESTAMP_KEY = "timestamps";

    /**
     * 心跳上报URL
     */
    private static final String REPORT_HEARTBEAT_URL = "http://tracking.tbit.com.cn:9090/adapter/api/reportHeartbeat";

    public Version() {
        // 实例化时输出服务名称和版本号
        System.out.printf("NAME:%s%n", NAME);
        System.out.printf("VER:%s%n", VERSION);
        System.out.printf("上报延迟:%s%n", RANDOM_DELAY_MILLIS);
    }

    public static void main(String[] args) throws Exception {
        new Version();
    }

    public static String getVersion() {
        return VERSION;
    }

    /**
     * 随机延迟毫秒数
     */
    private static final long RANDOM_DELAY_MILLIS = (long) (Math.random() * 50 * 1000);

    private static final ExecutorService executorService = new java.util.concurrent.ThreadPoolExecutor(1, 1,
            0L, TimeUnit.MILLISECONDS,
            new ArrayBlockingQueue<>(3));

    /**
     * 心跳上报，每分钟上报一次
     */
    @Scheduled(cron = "0 0/1 * * * ?")
    public void reportHeartbeatTask() {
        // 随机延迟0~50秒，确保不同服务心跳上报时间分散
        executorService.submit(() -> {
            try {
                Thread.sleep(RANDOM_DELAY_MILLIS);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            try {
                HeartbeatData heartbeatData = new HeartbeatData();
                heartbeatData.setAppData(getAppInfo());
                heartbeatData.setAppIndicators(getAppIndicators());

                // 接口上报
                String jsonStr = heartbeatData.toString();
                String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
                String sign = MD5Util.md5(TOKEN + timestamp);

                Map<String, String> headers = new HashMap<>();
                headers.put(TIMESTAMP_KEY, timestamp);
                headers.put(SIGN_KEY, sign);

                String resp = HttpUtils.doJsonPost(REPORT_HEARTBEAT_URL, headers, jsonStr);

                if (!resp.contains("\"ret\":1")) {
                    log.warn("心跳上报失败：{}", resp);
                }
            } catch (Exception e) {
                log.warn(e.getMessage(), e);
            }
        });
    }

    /**
     * 获取当前App信息
     */
    private App getAppInfo() throws UnknownHostException {
        App appInfo = new App();
        appInfo.setClientIp(InetAddress.getLocalHost().getHostAddress());
        appInfo.setAppName(NAME);
        appInfo.setVersion(VERSION);
        appInfo.setAppDir(System.getProperty("user.dir"));
        appInfo.setHeartbeatTime(ZonedDateTime.now()); // 避免不同json序列化库规则带来的差异
        appInfo.setVerCompVersion(VER_COMP_VERSION);
        return appInfo;
    }

    /**
     * 获取当前指标数据
     */
    private List<AppIndicator> getAppIndicators() {
        List<AppIndicator> indicators = new ArrayList<>();
        indicators.addAll(getJvmThreadIndicators());
        indicators.addAll(getJvmMemoryIndicators());
        indicators.addAll(getTomcatThreadIndicators());
        return indicators;
    }

    /**
     * 获取JVM线程指标
     */
    private List<AppIndicator> getJvmThreadIndicators() {
        List<AppIndicator> list = new ArrayList<>();

        ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
        long startedCount = threadBean.getTotalStartedThreadCount();
        int count = threadBean.getThreadCount();
        int daemonCount = threadBean.getDaemonThreadCount();

        ThreadInfo[] threadInfos = threadBean.getThreadInfo(threadBean.getAllThreadIds());
        int newThreadCount = 0;
        int runnableThreadCount = 0;
        int blockedThreadCount = 0;
        int waitThreadCount = 0;
        int timeWaitThreadCount = 0;
        int terminatedThreadCount = 0;
        if (threadInfos != null) {
            for (ThreadInfo threadInfo : threadInfos) {
                if (threadInfo != null) {
                    switch (threadInfo.getThreadState()) {
                        case NEW:
                            newThreadCount++;
                            break;
                        case RUNNABLE:
                            runnableThreadCount++;
                            break;
                        case BLOCKED:
                            blockedThreadCount++;
                            break;
                        case WAITING:
                            waitThreadCount++;
                            break;
                        case TIMED_WAITING:
                            timeWaitThreadCount++;
                            break;
                        case TERMINATED:
                            terminatedThreadCount++;
                            break;
                        default:
                            break;
                    }
                } else {
                    terminatedThreadCount++;
                }
            }


            // 开启线程总数
            list.add(new AppIndicator(IndicatorType.JVM_THREAD, IndicatorKey.JVM_THREAD_STARTED_COUNT, String.valueOf(startedCount)));
            // 线程数
            list.add(new AppIndicator(IndicatorType.JVM_THREAD, IndicatorKey.JVM_THREAD_COUNT, String.valueOf(count)));
            // 守护线程总数
            list.add(new AppIndicator(IndicatorType.JVM_THREAD, IndicatorKey.JVM_THREAD_DAEMON_COUNT, String.valueOf(daemonCount)));
            // 新建线程数
            list.add(new AppIndicator(IndicatorType.JVM_THREAD, IndicatorKey.JVM_THREAD_NEW_COUNT, String.valueOf(newThreadCount)));
            // 运行线程数
            list.add(new AppIndicator(IndicatorType.JVM_THREAD, IndicatorKey.JVM_THREAD_RUN_COUNT, String.valueOf(runnableThreadCount)));
            // 阻塞线程数
            list.add(new AppIndicator(IndicatorType.JVM_THREAD, IndicatorKey.JVM_THREAD_BLOCKED_COUNT, String.valueOf(blockedThreadCount)));
            // 等待线程数
            list.add(new AppIndicator(IndicatorType.JVM_THREAD, IndicatorKey.JVM_THREAD_WAITING_COUNT, String.valueOf(waitThreadCount)));
            // 等待超时线程数
            list.add(new AppIndicator(IndicatorType.JVM_THREAD, IndicatorKey.JVM_THREAD_TIME_WAITING_COUNT, String.valueOf(timeWaitThreadCount)));
            // 死亡线程数
            list.add(new AppIndicator(IndicatorType.JVM_THREAD, IndicatorKey.JVM_THREAD_TERMINATED_COUNT, String.valueOf(terminatedThreadCount)));
            // 死锁线程数
            long[] ids = threadBean.findDeadlockedThreads();
            int deadlockCount = ids == null ? 0 : ids.length;
            list.add(new AppIndicator(IndicatorType.JVM_THREAD, IndicatorKey.JVM_THREAD_DEADLOCK_COUNT, String.valueOf(deadlockCount)));

        }
        return list;
    }

    /**
     * 获取JVM内存指标
     */
    private List<AppIndicator> getJvmMemoryIndicators() {
        // 单位MB
        long totalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        long freeMemory = Runtime.getRuntime().freeMemory() / 1024 / 1024;
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
        // jvm已使用内存
        long userMemory = totalMemory - freeMemory;
        // jvm内存使用率
        String userRate = new DecimalFormat("#.##").format(userMemory * 1.0 / totalMemory);
        List<AppIndicator> list = new ArrayList<>();
        list.add(new AppIndicator(IndicatorType.JVM_MEMORY, IndicatorKey.JVM_MEMORY_MAX, String.valueOf(maxMemory)));
        list.add(new AppIndicator(IndicatorType.JVM_MEMORY, IndicatorKey.JVM_MEMORY_TOTAL, String.valueOf(totalMemory)));
        list.add(new AppIndicator(IndicatorType.JVM_MEMORY, IndicatorKey.JVM_MEMORY_FREE, String.valueOf(freeMemory)));
        list.add(new AppIndicator(IndicatorType.JVM_MEMORY, IndicatorKey.JVM_MEMORY_USER, String.valueOf(userMemory)));
        list.add(new AppIndicator(IndicatorType.JVM_MEMORY, IndicatorKey.JVM_MEMORY_USER_RATE, userRate));
        return list;
    }

    /**
     * 获取Tomcat线程指标
     */
    private List<AppIndicator> getTomcatThreadIndicators() {
        List<AppIndicator> list = new ArrayList<>();
        try {
            TomcatWebServer webServer = (TomcatWebServer) applicationContext.getWebServer();
            ThreadPoolExecutor executor = (ThreadPoolExecutor) webServer.getTomcat().getConnector().getProtocolHandler().getExecutor();
            // 获取当前线程池中线程数量
            int poolSize = executor.getPoolSize();
            // 获取正在执行任务的线程数量
            int activeCount = executor.getActiveCount();
            // 获取等待执行的任务数量
            int queueSize = executor.getQueue().size();

            list.add(new AppIndicator(IndicatorType.TOMCAT_THREAD, IndicatorKey.TOMCAT_THREAD_COUNT, String.valueOf(poolSize)));
            list.add(new AppIndicator(IndicatorType.TOMCAT_THREAD, IndicatorKey.TOMCAT_THREAD_RUN_COUNT, String.valueOf(activeCount)));
            list.add(new AppIndicator(IndicatorType.TOMCAT_THREAD, IndicatorKey.TOMCAT_THREAD_IDLE_COUNT, String.valueOf((poolSize - activeCount))));
            list.add(new AppIndicator(IndicatorType.TOMCAT_THREAD, IndicatorKey.TOMCAT_THREAD_WAIT_COUNT, String.valueOf(queueSize)));
        } catch (Exception e) {
            log.warn(e.getMessage(), e);
        }
        return list;
    }
}

/**
 * 心跳
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
class HeartbeatData {
    /**
     * APP数据
     */
    private App appData;
    /**
     * APP指标数据
     */
    private List<AppIndicator> appIndicators;

    @Override
    public String toString() {
        return new SimpleJsonBuilder()
                .add("appData", appData)
                .add("appIndicators", appIndicators)
                .toString();
    }

}

/**
 * App信息
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
class App {
    /**
     * 程序名称
     */
    private String appName;
    /**
     * 版本号
     */
    private String version;

    /**
     * 程序目录
     */
    private String appDir;

    /**
     * 客户端ip
     */
    private String clientIp;
    /**
     * 心跳时间
     */
    private ZonedDateTime heartbeatTime;
    /**
     * VERSION组件版本
     */
    private String verCompVersion;

    @Override
    public String toString() {
        return new SimpleJsonBuilder()
                .add("appName", appName)
                .add("version", version)
                .add("appDir", appDir)
                .add("clientIp", clientIp)
                .add("heartbeatTime", heartbeatTime)
                .add("verCompVersion", verCompVersion)
                .toString();
    }
}

/**
 * App指标信息
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
class AppIndicator {

    /**
     * 指标类型
     */
    private Integer indicatorType;

    /**
     * 指标键
     */
    private String indicatorKey;

    /**
     * 指标数值
     */
    private String indicatorValue;

    @Override
    public String toString() {
        return new SimpleJsonBuilder()
                .add("indicatorType", indicatorType)
                .add("indicatorKey", indicatorKey)
                .add("indicatorValue", indicatorValue)
                .toString();
    }
}

/**
 * 指标类型
 */
interface IndicatorType {
    /**
     * jvm内存
     */
    Integer JVM_MEMORY = 0;
    /**
     * jvm线程
     */
    Integer JVM_THREAD = 1;

    /**
     * jvm内存
     */
    Integer TOMCAT_THREAD = 2;
}

/**
 * 指标键类型
 */
interface IndicatorKey {
    /*===============jvm线程===================*/
    /**
     * 开启线程总数
     */
    String JVM_THREAD_STARTED_COUNT = "startedCount";
    /**
     * 线程数
     */
    String JVM_THREAD_COUNT = "count";
    /**
     * 守护线程数
     */
    String JVM_THREAD_DAEMON_COUNT = "daemonCount";
    /**
     * 新建线程数
     */
    String JVM_THREAD_NEW_COUNT = "newCount";
    /**
     * 运行线程数
     */
    String JVM_THREAD_RUN_COUNT = "runCount";
    /**
     * 阻塞线程数
     */
    String JVM_THREAD_BLOCKED_COUNT = "blockedCount";
    /**
     * 等待线程数
     */
    String JVM_THREAD_WAITING_COUNT = "waitingCount";
    /**
     * 等待超时线程数
     */
    String JVM_THREAD_TIME_WAITING_COUNT = "timeWaitingCount";
    /**
     * 死亡线程数
     */
    String JVM_THREAD_TERMINATED_COUNT = "terminatedCount";
    /**
     * 死锁线程数
     */
    String JVM_THREAD_DEADLOCK_COUNT = "deadlockCount";

    /*===============jvm内存===================*/
    /**
     * 最大可申请内存
     */
    String JVM_MEMORY_MAX = "max";
    /**
     * 可使用内存
     */
    String JVM_MEMORY_TOTAL = "total";
    /**
     * 空闲内存
     */
    String JVM_MEMORY_FREE = "free";
    /**
     * 使用内存
     */
    String JVM_MEMORY_USER = "user";
    /**
     * 使用率
     */
    String JVM_MEMORY_USER_RATE = "userRate";

    /*===============tomcat线程===================*/
    /**
     * 线程池中线程数
     */
    String TOMCAT_THREAD_COUNT = "count";
    /**
     * 运行的线程数
     */
    String TOMCAT_THREAD_RUN_COUNT = "runCount";
    /**
     * 空闲的线程数
     */
    String TOMCAT_THREAD_IDLE_COUNT = "idleCount";
    /**
     * 队列中等待执行的线程数
     */
    String TOMCAT_THREAD_WAIT_COUNT = "waitCount";

}

final class SimpleJsonBuilder {
    private final StringBuilder sb = new StringBuilder("{");
    private boolean isFirst = true;

    private void checkFirst() {
        if (!isFirst) {
            sb.append(",");
        }
        isFirst = false;
    }

    public SimpleJsonBuilder add(String key, Number value) {
        checkFirst();
        sb.append("\"").append(key).append("\":").append(value);
        return this;
    }

    public SimpleJsonBuilder add(String key, Boolean value) {
        checkFirst();
        sb.append("\"").append(key).append("\":").append(value);
        return this;
    }

    public SimpleJsonBuilder add(String key, String value) {
        checkFirst();
        sb.append("\"").append(key).append("\":\"").append(escape(value)).append("\"");
        return this;
    }

    private String escape(String value) {
        if (value == null || value.isEmpty()) {
            return value;
        }
        final int len = value.length();
        final StringBuilder builder = new StringBuilder(len);
        char c;
        for (int i = 0; i < len; i++) {
            c = value.charAt(i);
            builder.append(escape(c));
        }
        return builder.toString();
    }

    private static final char[] alphabets = "0123456789abcdef".toCharArray();

    private static String escape(char c) {
        switch (c) {
            case '"':
                return "\\\"";
            case '\\':
                return "\\\\";
            case '\b':
                return "\\b";
            case '\t':
                return "\\t";
            case '\n':
                return "\\n";
            case '\f':
                return "\\f";
            case '\r':
                return "\\r";
            default:
                if (c < ' ' || //
                        (c >= '\u0080' && c <= '\u00a0') || //
                        (c >= '\u2000' && c <= '\u2010') || //
                        (c >= '\u2028' && c <= '\u202F') || //
                        (c >= '\u2066' && c <= '\u206F')//
                ) {
                    return "\\u" +//
                            alphabets[(c >> 12) & 15] +//
                            alphabets[(c >> 8) & 15] +//
                            alphabets[(c >> 4) & 15] +//
                            alphabets[(c) & 15];
                } else {
                    return Character.toString(c);
                }
        }
    }

    public SimpleJsonBuilder add(String key, List<?> value) {
        checkFirst();
        sb.append("\"").append(key).append("\":[");
        for (int i = 0; i < value.size(); i++) {
            if (i > 0) {
                sb.append(",");
            }
            Object o = value.get(i);
            if (o instanceof Number || o instanceof Boolean) {
                sb.append(o);
            } else if (o instanceof String) {
                sb.append("\"").append(o).append("\"");
            } else {
                sb.append(o.toString());
            }
        }
        sb.append("]");
        return this;
    }

    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    public SimpleJsonBuilder add(String key, ZonedDateTime value) {
        checkFirst();
        sb.append("\"").append(key).append("\":\"").append(value.format(formatter)).append("\"");
        return this;
    }

    public SimpleJsonBuilder add(String key, Object value) {
        checkFirst();
        sb.append("\"").append(key).append("\":").append(value);
        return this;
    }

    @Override
    public String toString() {
        return sb.append("}").toString();
    }
}

@Slf4j
class HttpUtils {
    public static String doJsonPost(String urlPath, Map<String, String> headers, String Json) {
        String result = "";
        BufferedReader reader = null;
        try {
            URL url = new URL(urlPath);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            conn.setRequestProperty("Connection", "Keep-Alive");
            conn.setRequestProperty("Charset", "UTF-8");
            conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
            conn.setRequestProperty("accept", "application/json");
            headers.forEach(conn::setRequestProperty);
            conn.setReadTimeout(8000);
            conn.setConnectTimeout(8000);

            byte[] writebytes = Json.getBytes();
            conn.setRequestProperty("Content-Length", String.valueOf(writebytes.length));
            OutputStream outwritestream = conn.getOutputStream();
            outwritestream.write(Json.getBytes());
            outwritestream.flush();
            outwritestream.close();
            if (conn.getResponseCode() == 200) {
                reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                result = reader.readLine();
            }
        } catch (Exception e) {
            log.warn("心跳请求出错", e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    log.warn("心跳请求出错", e);
                }
            }
        }
        return result;
    }
}


class MD5Util {
    private static final String[] HEX_DIGITS = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"};

    public static String md5(String origin) {
        String resultString = origin;
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            resultString = byteArrayToHexString(md.digest(origin.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception ignored) {
        }
        return resultString;
    }

    private static String byteArrayToHexString(byte[] b) {
        StringBuilder resultSb = new StringBuilder();
        for (byte value : b) {
            resultSb.append(byteToHexString(value));
        }
        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0) {
            n += 256;
        }
        int d1 = n / 16;
        int d2 = n % 16;
        return HEX_DIGITS[d1] + HEX_DIGITS[d2];
    }
}